How Packets are Received?

Sniffing and spoofing are the most common attacks on networks.
Sniffing consist of capturing all the packets transmitted over the network.
Spoofing consist of sending packets claiming to be from other computer.

Network Interface Card (NIC)

Machines are connected to networks through NIC, which is a physical or logical link between a machine and a network.
Each NIC has a hardware address called MAC address.
Every NIC in a network will hear all the frames on the wire.

How NIC works?

When a frame arrives via the medium (WiFi, Ethernet), it's copied inside the NIC memory, which checks the destination address in the header of the packet: if the the address matches the card MAC address, it is further copied into a buffer in the kernel, trough DMA.

Promiscuous Mode

Frames that are not designed to a given NIC are discarded.
However, when we set the promiscuous mode on, NIC passes all the frames received to the kernel.
If a sniffer program is registered with the kernel, it will be able to see all the packets.

Notice: in Wifi, this is called Monitor Mode.

BSD Packet Filter (BPF)

When sniffing network traffic, it's quite common that sniffers are only interested in certain types of packets (TCP or DNS).
Since there will be a lot of unwanted packets and we won't wast computing time, we can apply some filter.
Indeed, Unix OS support filtering at low level, so called BSD Packet Filter (BPF).
In detail, BPF allows a user-space program to attach a filter to a socket, which tells the kernel to discard unwanted packets as soon as possible.

This filter is often written in a human readable format using boolean operator and then compiled in a low-level code that could be interpreted by the BPF Pseudo-Machine.
A filter, as compiled BPF pseudo-code, can be attached to a socket through setsocketopt().

How this filter works?

When a packet is received by kernel, BPF will be invoked. An accepted packet will be pushed up to the protocol stack.

Pasted image 20250527152302.png

Packet Sniffing

Packet sniffing describes the process of capturing live data ad they flow across the network.

Receiving Packet using Socket

In C, we can set up an UDP socket as follows:

// Create the socket
int sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);

struct sockaddr_in server;
struct sockaddr_in server;
// Provide information about the server
memset((char *) &server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_addr.s_addr = htonl(INADRR_ANY);
server.sin_port_htons(9090);

if ( bind(sock,(struct sockaddr*) &server, sizeof(server)) < 0) 
	perror("ERROR on binding");

//Receive packets
while(1){
	bzero(buf,1500);
	recvfrom(sock, buf, 1500-1,0,
			(struct sockaddr *) &client, &lclientlen);
	printf("%s\n",buf);
}
close(sock);

Since we need to sniff any packets flowing on the network, regardless the destination IP or port number, we need another type of socket which is the raw socket.

  1. We create the raw socket
int sock = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));

In this way, the socket does not intercept the packet, it simply gets a copy.
Then, the third argument of the socket function specify the protocol: in our case with htons(ETH_P_ALL) we are indicating that packets of all protocols should be passed to the raw socket.

  1. Enabling the promiscuous mode
struct packet_mreq mr;
mr.mr_type = PACKET_MR_PROMISC;
setsockopt(sock, SOL_PACKET, PACKET_ADD_MEMBERSHIP, &mr, sizeof(mr));
  1. Wait for packets: by using recvfrom to wait for packets. Once a packet arrives, the raw socket will receive a copy of it through this API.

Here, there's no filter. If we want to add filters we have to use setsockeopt with the SO_ATTACH_FILTER option name.
However, this approach is not portable across different OS.
Then, this program does not explore any optimization to improve performance: if there's a lot of network traffic, our program will miss some packets.

Packet Sniffing using pcap API

The pcap library has been implemented to solve such troubles.
It still uses raw sockets internally, but its API is standard across all platforms.
Furthermore, it allows programmer to specify filtering rules using human readable boolean expressions.

How?

  1. Open live pcap session on NIC with name eth3
handle = pcap_open_live("eth3", BUFSIZ, 1, 1000, errbuf);

This will initialize a row socket and set the NIC into promiscuous mode (the 1 in the args).

  1. Set the filter
char filter_exp[ ] = "ip proto icmp"
pcap_compile(handle, &fp, filter_exp, 0, net);
pcap_setfilter(handle, &fp);

This snippet will compiles the filter expression and then set up the BPF filter.

  1. Capture packets
pcap_loop(handle,-1, got_packet, NULL);

Here:

  • -1 stands for "loop"
  • got_packet is a function callback that is called whenever a packet is captured by pcap.
  1. Compilation
gcc -o sniff sniff.c -lpcap

Processing Captured Packet

If we want to process a packet, we can change the got_packet function so that it accepts a pointer to the packet as an unsigned char *. In this way a packet can be seen as a sequence of chars, so by typecasting a pointer of char buffer into a pointer to an ethernet header struct (struct header*) we can easily get the field we want. Indeed:

void got_packet(u_char* args, const struct pcap_pkthdr* header, const u_char* packet){
	struct ethheader* eth = (struct ethheader*) packet;
	if(ntohs(eth->ether_type)==0x8000){ // 0x8000 : IP type
		struct ipheader* ip = (struct ipheader*) (packet + sizeof(struct ethheader));
		printf("From: %s\n", inet_ntoa(ip->iph_sourceip));
		printf("To: %s\n", inet_ntoa(ip->iph_dstip));

		switch(ip->iph_protocol){
			case IPPROT_TCP:
			printf("Protocol: TCP\n");
			break;	

			case IPPROT_UCP:
			printf("Protocol: UCP\n");
			break;	

			
			case IPPROT_ICMP:
			printf("Protocol: ICMP\n");
			break;	

			default:break;
		}
	}
}

Packet Spoofing

When some critical information in the packet is forget, we refer to it as packet spoofing.

Sending Normal Packets using Socket

As we have done for the sniffing attack, we have to create a socket, provide the information about the destination (IP + port) and use sendto to send out packet.

Sending Spoofed Packets using Raw Sockets

By using the typical socket to sent packets, we do not have much control over the header fields: indeed, we can only fill a few header fields such as the destination IP and the destination port. Other field, such as the source IP address, packet length.. are set by OS itself.

Another time, we can use raw sockets, but we have to:

  • construct the packet in a buffer, including the IP header and all its subsequent headers.
  • sending the packet created.

Sending Created Packets

For sending a packet:

  1. Create the socket
int sock = socket(AF_INET, SOCK_RAW, IPPROTO_RAW);
  1. Use setsockopt() to enable IP_HDRINCL on the socket, which stands for "header included".
setsockopt(sock, IPPROTO_IP,IP_HDRINCL, &enable, sizeof(enable));
  1. For a raw socket programming, since the destination information is already included in the IP header, there's no need to fill in all the fields in sockaddr_in structure, other than setting the family information and the destination IP address.
dest_info.sin_family = AF_INET;
dest_info.sin_addr = ip -> iph_destip;
  1. Since the pocket is a raw one, the system will send the IP packet as is.

Creating ICMP Packets

To construct a ICMP Echo request, we have to fill:

  1. ICMP Header: we find the starting point of the ICMP header, and typecast it to the ICMP structure. Then we can fill it.
  2. IP Header: typecast the buffer to the IP structure. Then we can fill it
  3. Send the packet by send_raw_ip_packet(ip)

Notice: since we construct a ICMP Echo request, the payload is optional.

Creating UDP Packets

The procedure is similar, but here we have to include the payload.

  1. Fill the UPD Data field.
  2. Fill the UDP Header.
  3. Fill in the IP Header.

Sniffing and Then Spoofing

In many situations, we need first to capture the packets first and then spoof a response based on captured packets.

Example: UDP

Procedure:

  • Use PCAP API to capture the packets of interests
  • Make a copy from the captured packet
  • Replace the UDP data field with the new massage and swap the source and the destination fields
  • Send out the spoofed reply

The sniffing part is the same as the one explained above.
The only part that change is the callback function got_packet: indeed, it will invoke the following function

void spoof_reply(struct ipheader* ip){
	const char buffer[1500];
	int ip_header_len = ip->iph_ihl *4;
	struct udpheader* udp = (struct udpheader*)((u_char*)ip+ip_header_len);

	if(ntohs(udp->udp_dport) != 9999){
		//We spoof UDP packet with destination port 9999
		return;
	}

	//Step 1: Make a copy from the original packet
	memset((char*) buffer, 0, 1500);
	memcpy((char*) buffer, ip, ntohs(ip->iph_len));
	struct ipheader * newip = (struct ipheader*) buffer;
	struct udpheader * newudp = (struct udpheader*) (buffer + ip_header_len);
	char* data = (char*) newudp +sizeof(struct udpheader);

	//Step 2: Create UDP payload
	const char* msg = "This is a spoofed reply!\n";
	int data_len = strlen(msg);
	strncpy(data, msg, data_len);

	//Step 3: Construct UDP Header
	newudp->udp_sport = udp->udp_dport;
	newudp->udp_dport = udp->udp_sport;
	newudp->udp_ulen = htons(sizeof(struct udpheader) + data_len); 
	newudp->udp_sum = 0;

	//Step 4: Construct IP header 
	newip->iph_sourceip = ip->iph_destip;
	newip->iph_destip = ip->iph_sourceip;
	newip->iph_ttl = 50; //Rest the TTL field
	newip->iph_len = htons(sizeof(struct ipheader)+sizeof(struct udpheader)+data_len);

	//Step 5: Send out spoofed IP packet
	send_raw_ip_packet(newip);
}

Snoofing with Scapy

This could be easily done via Python, by using Scapy library.

Scapy vs C

Pro Con
Scapy Easy packets construction Slow forwarding
C Fast forwarding Hard packets construction

An "Hybrid approach" is the best choice:

  • use Scapy to construct packets
  • use C to send packets (or minor modification)

Endianness

With the term "Endianness" we refer to the order in which a multi-byte data(any type different from char actually) is stored in memory.

There are 2 types of endianness in the recent architecture:

  • Little Endian: most significant byte at the highest address
  • Big Endian: most significant byte at the lowest address

The following picture will explain better the concept.
Pasted image 20250528143411.png

Clearly, computers with different byte orders will misunderstand each other messages.
Indeed, there's a common order for communication, called network order, which is the same as Big Endian order.

That's why, in our examples, we use htons,htonl: we use this macro to convert from the "host order" (h) to the "network order" (n).
The following table summarizes the possible conversion:

Macro Description
htons Unsigned short integer from host order to network order
htonl Unsigned integer from host order to network order
ntohs Unsigned short integer from network order to host order
ntohl Unsigned integer from network order to host order